/*
 * OREKIT-X
 * Copyright 2002-2008 CS Communication & Systemes
 * 
 * Parts of this software package have been licensed to CS
 * Communication & Systemes (CS) under one or more contributor license
 * agreements.  See the NOTICE file distributed with this work for
 * additional information.
 *  
 * This is an experimental copy of OREKIT from www.orekit.org.
 * Please use the original OREKIT from orekit.org for normal work
 * unrelated to this research project.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you
 * may not use this file except in compliance with the License.  You
 * may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.  See the License for the specific language governing
 * permissions and limitations under the License.
 */
package ore.time;

import ore.errors.OrekitException;
import ore.time.date.CommonYearFactory;
import ore.time.date.GregorianFactory;
import ore.time.date.JulianFactory;
import ore.time.date.LeapYearFactory;
import ore.time.date.MonthDayFactory;
import ore.time.date.ProlepticJulianFactory;
import ore.time.date.YearFactory;

import java.text.DecimalFormat;

/** 
 * This class represents a date in its components: year, month and
 * day.
 * 
 * This class uses the astronomical convention for calendars,
 * which is also the convention used by <code>java.util.Date</code>:
 * a year zero is present between years -1 and +1, and 10 days are
 * missing in 1582. The calendar used around these special dates are:
 * <ul>
 *   <li>up to 0000-12-31 : proleptic julian calendar</li>
 *   <li>from 0001-01-01 to 1582-10-04: julian calendar</li>
 *   <li>from 1582-10-15: gregorian calendar</li>
 * </ul>
 * 
 * Instances of this class are immutable.
 * 
 * @see TimeComponents
 * @see DateTimeComponents
 * @author Luc Maisonobe
 */
public class DateComponents
    extends Object
    implements java.lang.Comparable<DateComponents>,
               java.io.Serializable
{
    /** 
     * Reference epoch for julian dates: -4712-01-01.
     * 
     * Both <code>java.util.Date</code> and {@link DateComponents}
     * classes follow the astronomical conventions and consider a year
     * 0 between years -1 and +1, hence this reference date lies in
     * year -4712 and not in year -4713 as can be seen in other
     * documents or programs that obey a different convention (for
     * example the <code>convcal</code> utility).
     */
    public static final DateComponents JULIAN_EPOCH          = new DateComponents(-4712,  1,  1);
    /** 
     * Reference epoch for modified julian dates: 1858-11-17.
     */
    public static final DateComponents MODIFIED_JULIAN_EPOCH = new DateComponents( 1858, 11, 17);
    /** 
     * Reference epoch for 1950 dates: 1950-01-01.
     */
    public static final DateComponents FIFTIES_EPOCH         = new DateComponents( 1950,  1,  1);
    /** 
     * Reference epoch for GPS weeks: 1980-01-06.
     */
    public static final DateComponents GPS_EPOCH             = new DateComponents( 1980,  1,  6);
    /** 
     * J2000.0 Reference epoch: 2000-01-01.
     */
    public static final DateComponents J2000_EPOCH           = new DateComponents( 2000,  1,  1);
    /** 
     * Java Reference epoch: 1970-01-01.
     */
    public static final DateComponents JAVA_EPOCH            = new DateComponents( 1970,  1,  1);

    /** 
     * Format for years.
     */
    private static final DecimalFormat FOUR_DIGITS = new DecimalFormat("0000");
    /** 
     * Format for months and days.
     */
    private static final DecimalFormat TWO_DIGITS  = new DecimalFormat("00");
    /** 
     * Offset between J2000 epoch and modified julian day epoch.
     */
    private static final int MJD_TO_J2000 = 51544;


    /** 
     * Year number.
     */
    public final int year;
    /** 
     * Month number.
     */
    public final int month;
    /** 
     * Day number.
     */
    public final int day;


    /** 
     * @param year Year number (may be 0 or negative for BC years)
     * @param month Month number from 1 (to 12)
     * @param day Day number from 1 (to 31)
     * 
     * @exception IllegalArgumentException Parameters out of range
     */
    public DateComponents(int year, int month, int day)
        throws IllegalArgumentException
    {
        super();
        /*
         * Preliminary range check to avoid
         * ArrayIndexOutOfBoundsException in MonthDayFactory
         */
        if ((month < 1) || (month > 12))
            throw OrekitException.createIllegalArgumentException("non-existent month {0}", month);
        else {
            /*
             * Check date from the J2000 day
             */
            DateComponents check = new DateComponents(this.getJ2000Day());
            /*
             * Check the parameters for mismatch
             * (i.e. invalid date components, like 29 february on non-leap years)
             */
            if ((year != check.year) || (month != check.month) || (day != check.day)) {
                throw OrekitException.createIllegalArgumentException("non-existent date {0}-{1}-{2}",
                                                                     year, month, day);
            }
            else {
                this.year  = year;
                this.month = month;
                this.day   = day;
            }
        }
    }

    /** 
     * @param year Year number (may be 0 or negative for BC years)
     * @param dayNumber Day number in the year from 1 (to 366)
     * 
     * @exception IllegalArgumentException Day number is out of range
     * with respect to year
     */
    public DateComponents(int year, int dayNumber)
        throws IllegalArgumentException
    {
        this(J2000_EPOCH, new DateComponents(year - 1, 12, 31).getJ2000Day() + dayNumber);
        if (dayNumber != this.getDayOfYear()) {
            throw OrekitException.createIllegalArgumentException("no day number {0} in year {1}",
                                                                 dayNumber, year);
        }
    }
    /** 
     * @param offset Offset with respect to a {@link #J2000_EPOCH}
     */
    public DateComponents(int offset) {
        super();
        /*
         * We follow the astronomical convention for calendars:
         * we consider a year zero and 10 days are missing in 1582
         * from 1582-10-15: gregorian calendar
         * from 0001-01-01 to 1582-10-04: julian calendar
         * up to 0000-12-31 : proleptic julian calendar
         */
        YearFactory yFactory = GregorianFactory.Instance;
        if (offset < -152384) {
            if (offset > -730122) {
                yFactory = JulianFactory.Instance;
            } else {
                yFactory = ProlepticJulianFactory.Instance;
            }
        }
        this.year = yFactory.getYear(offset);
        int dayInYear = (offset - yFactory.getLastJ2000DayOfYear(year - 1));
        MonthDayFactory mdFactory;
        {
            /*
             * Handle month/day according to the year being a common
             * or leap year
             */
            if (yFactory.isLeap(this.year))
                mdFactory = LeapYearFactory.Instance;
            else
                mdFactory = CommonYearFactory.Instance;
        }
        this.month = mdFactory.getMonth(dayInYear);
        this.day   = mdFactory.getDay(dayInYear, month);
    }
    /** 
     * Build a date from its offset with respect to a reference epoch.
     * 
     * This constructor is mainly useful for building a date from a
     * modified julian day (using {@link #MODIFIED_JULIAN_EPOCH}).
     * 
     * @param epoch Reference epoch
     * @param offset Offset with respect to a reference epoch
     */
    public DateComponents(DateComponents epoch, int offset) {
        this(epoch.getJ2000Day() + offset);
    }


    /** 
     * @return Year number (may be 0 or negative for BC years)
     */
    public int getYear() {
        return this.year;
    }
    /** 
     * @return Month number from 1 to 12
     */
    public int getMonth() {
        return this.month;
    }
    /** 
     * @return Day number from 1 to 31
     */
    public int getDay() {
        return this.day;
    }
    /** 
     * @return Day number with respect to the J2000 epoch
     */
    public int getJ2000Day() {
        YearFactory yFactory = GregorianFactory.Instance;
        if (this.year < 1583) {
            if (this.year < 1) {
                yFactory = ProlepticJulianFactory.Instance;
            }
            else if ((this.year < 1582) || (month < 10) || ((month < 11) && (day < 5))) {
                yFactory = JulianFactory.Instance;
            }
        }
        MonthDayFactory mdFactory;
        {
            /*
             * Handle month/day according to the year being a common
             * or leap year
             */
            if (yFactory.isLeap(this.year))
                mdFactory = LeapYearFactory.Instance;
            else
                mdFactory = CommonYearFactory.Instance;
        }
        return (yFactory.getLastJ2000DayOfYear(this.year - 1) +
                mdFactory.getDayInYear(this.month, this.day));
    }
    /** 
     * @return Modified Julian Day
     */
    public int getMJD() {
        return (MJD_TO_J2000 + this.getJ2000Day());
    }
    /** 
     * @return Day of week as a number between 1 (Monday) and 7
     * (Sunday).
     */
    public int getDayOfWeek() {
        int dow = ((this.getJ2000Day() + 6) % 7); // result is between -6 and +6
        if (dow < 1) 
            return (dow + 7);
        else
            return dow;
    }
    /** 
     * @return Day number in year is between 1 (January 1st) and
     * either 365 or 366 (inclusive), depending on the year.
     */
    public int getDayOfYear() {
        int j2000Day = this.getJ2000Day();
        int startOfYear = (new DateComponents(this.year - 1, 12, 31).getJ2000Day());
        return (j2000Day - startOfYear);
    }
    /** 
     * @return ISO-8601 date.
     */
    public String toString() {
        StringBuffer string = new StringBuffer();
        string.append(FOUR_DIGITS.format(this.year)).append('-');
        string.append(TWO_DIGITS.format(this.month)).append('-');
        string.append(TWO_DIGITS.format(this.day));
        return string.toString();
    }
    public int compareTo(DateComponents that) {
        int thisDay = this.getJ2000Day();
        int thatDay = that.getJ2000Day();
        if (thisDay < thatDay)
            return -1;
        else if (thisDay > thatDay)
            return 1;
        else
            return 0;
    }
    public boolean equals(Object that) {
        if (this == that)
            return true;
        else if (that instanceof DateComponents){
            DateComponents thatDate = (DateComponents) that;
            return ((this.year == thatDate.year) &&
                    (this.month == thatDate.month) &&
                    (this.day == thatDate.day));
        }
        else
            return false;
    }
    public int hashCode() {
        return ((this.year << 16) ^ (this.month << 8) ^ this.day);
    }
}
